package co.baiku.boot.core.orm.dao.impl;

import co.baiku.boot.common.exception.ApiException;
import co.baiku.boot.common.exception.SQLTemplateException;
import co.baiku.boot.common.spring.SpringContext;
import co.baiku.boot.common.tools.ObjectTools;
import co.baiku.boot.common.tools.StringTools;
import co.baiku.boot.config.AjavaerConfig;
import co.baiku.boot.core.orm.bin.RunSQL;
import co.baiku.boot.core.orm.bin.TemplateBuilder;
import co.baiku.boot.core.orm.dao.BaseDAO;
import co.baiku.boot.core.orm.po.TenantPo;
import co.baiku.boot.core.orm.po.Po;
import co.baiku.boot.core.orm.tools.FreemarkerTools;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.metamodel.EntityType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * DAO查询基类
 *
 * @param <T>  实体类
 * @param <ID> 主键类型
 */
public class BaseDAOImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, Serializable> implements BaseDAO<T, Serializable>, InitializingBean {
    private final Logger log = LoggerFactory.getLogger(BaseDAOImpl.class);
    private final EntityManager entityManager;
    private final Class<T> domainClass;
    private final Map<Class<?>, Resource> sqlResources = new ConcurrentHashMap<>();
    private final Map<Class<?>, Long> lastModified = new ConcurrentHashMap<>();
    private final Map<Class<?>, Map<String, Template>> templateIdMap = new ConcurrentHashMap<>();
    private Configuration configuration;
    private RunSQL runSQL;
    private AjavaerConfig ajavaerConfig;

    public BaseDAOImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.entityManager = em;
        this.domainClass = domainClass;
    }

    public BaseDAOImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager em) {
        super(entityInformation, em);
        this.entityManager = em;
        this.domainClass = entityInformation.getJavaType();
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable) {
        return super.findAll(spec, pageable);
    }

    @Override
    public T getById(Serializable id) {
        Optional<T> findOne = findById(id);
        T one = null;
        if (findOne.isPresent()) {
            one = findOne.get();
        }
        if (one instanceof TenantPo && ((TenantPo) one).getDel()) {
            return null;
        }
        return one;
    }

    @Override
    public List<T> findByIdIn(Collection<Serializable> ids) {
        List<T> allById = findAllById(ids);
        if (!allById.isEmpty()) {
            T t = allById.get(0);
            if (t instanceof TenantPo) {
                return allById.stream().filter(item -> !((TenantPo) item).getDel()).collect(Collectors.toList());
            }
        }
        return allById;
    }

    @Override
    public <S extends T> S save(S entity) {
        if (entity instanceof TenantPo) {
            Boolean del = ((TenantPo) entity).getDel();
            if (del == null) {
                ((TenantPo) entity).setDel(false);
            }
        }
        return super.save(entity);
    }

    @Override
    public int templateUpdate(String templateId, Map<?, ?> params) {
        String sql = templateSql(templateId, params);
        return runSQL.executeUpdate(sql, params);
    }

    @Override
    public int templateUpdate(EntityManager entityManager, String templateId, Map<?, ?> params) {
        String sql = templateSql(templateId, params);
        return runSQL.executeUpdateWithoutTx(entityManager, sql, params);
    }

    @Override
    public List<Map<String, Object>> templateQuery(String templateId, Map<?, ?> params) {
        String sql = templateSql(templateId, params);
        return runSQL.executeMap(sql, params);
    }

    @Override
    public List<T> templateQuery(String templateId, Map<?, ?> params, Class<T> clazz) {
        String sql = templateSql(templateId, params);
        return runSQL.executeObject(sql, clazz, params);
    }

    @Override
    public void delete(T entity) {
        if (entity instanceof TenantPo) {
            ((TenantPo) entity).setDel(true);
            save(entity);
        } else {
            super.delete(entity);
        }
    }

    @Override
    public int dynamicUpdate(T entity) {
        if (this.runSQL == null) {
            this.afterPropertiesSet();
        }
        return runSQL.dynamicUpdate((Po) entity);
    }

    @Override
    public void deleteAllInBatch() {
        throw ApiException.of("禁止调用");
    }

    @Override
    public void deleteAll() {
        throw ApiException.of("禁止调用");
    }

    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        if (!ObjectTools.isBlank(entities)) {
            entities.forEach(this::delete);
        }
    }

    @Override
    public TemplateBuilder templateBuilder(String templateId) {
        return new TemplateBuilder(getTemplate(templateId));
    }

    @Override
    public String templateSql(String templateId, Map<?, ?> params) {
        return FreemarkerTools.processTemplate(getTemplate(templateId), params).trim();
    }

    @Override
    public String templateSql(String templateId) {
        return templateSql(templateId, new HashMap());
    }

    private Template getTemplate(String templateId) {
        if (this.runSQL == null || !templateIdMap.containsKey(domainClass)) {
            initSqlTemplate();
        }
        if (ajavaerConfig.getDebug()) {//debug模式时,如果模版变化,自动重载模版
            reloadIfPossible();
        }
        Map<String, Template> templateMap = templateIdMap.get(domainClass);
        if (templateMap == null) {
            throw new SQLTemplateException("template for domainClass [" + domainClass + "] not found");
        }
        Template template = templateMap.get(parseTemplateId(templateId));
        if (template == null) {
            throw new SQLTemplateException("template " + parseTemplateId(templateId) + " not found");
        }
        return template;
    }

    private String parseTemplateId(String id) {
        return domainClass.getName() + "." + id;
    }

    private void initSqlTemplate() {
        this.runSQL = SpringContext.getBean(RunSQL.class);
        this.ajavaerConfig = SpringContext.getBean(AjavaerConfig.class);
        this.configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        try {
            Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities();
            for (EntityType<?> entity : entities) {
                Resource resource = getResource(entity.getJavaType().getTypeName());
                if (resource != null && resource.exists()) {
                    sqlResources.put(entity.getJavaType(), resource);
                    lastModified.put(entity.getJavaType(), resource.lastModified());
                    loadTemplate(entity.getJavaType(), resource);
                }
            }
        } catch (Exception e) {
            log.error("read sql template error:" + e.getMessage(), e);
            System.exit(-1);
        }
    }

    private Resource getResource(String classTypeName) {
        String templateLocation = "classpath*:sqls/";
        String suffixPattern = ".sftl";
        return SpringContext.getResource(templateLocation + classTypeName.replaceAll("\\.", StringTools.VIRGULE) + suffixPattern);
    }

    private void loadTemplate(Class<?> clazz, Resource resource) throws Exception {
        if (resource != null && resource.exists()) {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StringTools.UTF8))) {
                String line;
                String templateId = null;
                StringBuilder stringBuilder = new StringBuilder();
                while ((line = bufferedReader.readLine()) != null) {
                    line = line.trim();
                    if (line.startsWith("--")) {
                        continue;
                    }
                    //get template id
                    if (line.startsWith("##")) {
                        if (stringBuilder.length() > 0) {
                            putTemplate(templateId, stringBuilder, clazz);
                            stringBuilder.delete(0, stringBuilder.length() - 1);
                        }
                        templateId = clazz.getTypeName() + "." + line.replaceAll("##", "").split("//")[0].trim();
                    } else {
                        //read template
                        stringBuilder.append(line).append("\n");
                    }
                }
                if (stringBuilder.length() > 0 && templateId != null) {
                    putTemplate(templateId, stringBuilder, clazz);
                }
            } catch (Exception e) {
                log.error("Parse " + clazz.getName() + " SQL template exception:" + e.getMessage(), e);
                throw e;
            }
        }
    }

    private void putTemplate(String templateId, StringBuilder stringBuilder, Class<?> clazz) throws IOException {
        if (stringBuilder == null || stringBuilder.length() == 0) {
            throw new SQLTemplateException("template content can not be empty");
        }
        if (StringTools.isBlank(templateId)) {
            throw new SQLTemplateException("templateId can not be null,template:" + stringBuilder.toString());
        }
        if (clazz == null) {
            throw new SQLTemplateException("clazz can not be null,template:" + stringBuilder.toString());
        }
        //put template
        Template template = new Template(templateId, stringBuilder.toString(), configuration);
        Map<String, Template> templateMap = templateIdMap.get(clazz);
        if (templateMap == null) {
            templateMap = new ConcurrentHashMap<>();
            templateIdMap.put(clazz, templateMap);
        }
        templateMap.put(templateId, template);
    }


    private void reloadIfPossible() {
        try {
            Long lastModified = this.lastModified.get(domainClass);
            Resource resource = sqlResources.get(domainClass);
            if (resource != null) {
                long newLastModified = resource.lastModified();
                //check modified for cache.
                if (lastModified == null || newLastModified > lastModified) {
                    this.lastModified.put(domainClass, newLastModified);
                    //process template.
                    loadTemplate(domainClass, resource);
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    @Override
    public <S extends T> Iterable<S> batchSave(Iterable<S> var1) {
        int size = SpringContext.env().getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", Integer.class, 1000);
        Iterator<S> iterator = var1.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            entityManager.persist(iterator.next());
            index++;
            if (index % size == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        if (index % size != 0) {
            entityManager.flush();
            entityManager.clear();
        }
        return var1;
    }

    @Override
    public <S extends T> Iterable<S> batchUpdate(Iterable<S> var1) {
        int size = SpringContext.env().getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", Integer.class, 1000);
        Iterator<S> iterator = var1.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            entityManager.merge(iterator.next());
            index++;
            if (index % size == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        if (index % size != 0) {
            entityManager.flush();
            entityManager.clear();
        }
        return var1;
    }

    @Override
    public void afterPropertiesSet() {
        if (this.runSQL == null) {
            initSqlTemplate();
        }
    }
}
